﻿using Google.Protobuf;
using System;
using System.Collections.Concurrent;
using System.Net;
using System.Threading;
using UnityEngine;

namespace OutOfUnity
{
    public class NetworkMethod
    {
        #region InternalVariable
        private int type_ = 0;
        private ProtocolFinder finder_ = null;
        private NetworkingReceiver receiver_ = null;
        private Thread recvThread_ = null;
        private Thread sendThread_ = null;
        private Boolean running_ = false;
        private Boolean broken_ = false;
        private String ip_ = "";
        private int port_ = 0;
        private ISocket socket_ = null;
        private ConcurrentQueue<NetEvent> input_ = new ConcurrentQueue<NetEvent>();
        private ConcurrentQueue<NetEvent> output_ = new ConcurrentQueue<NetEvent>();
        private Network.OnPingTask onPingTask_ = null;
        private int pingIntval_ = 0;
        private float pingDelay_ = 0;
        private object userObject_ = null;
        private int SentPacket_ = 0;
        private int RecvPacket_ = 0;
        public ProtocolFinder Transport
        {
            get
            {
                return finder_;
            }
        }
        #endregion
        public bool connect(int type, String ip, int port, object userObject = null, Network.OnPingTask pingTask = null, int pingIntval = 3000)
        {
            return internalConnect(type, ip, port, userObject, pingTask, pingIntval);
        }
        public void stop()
        {
            internalStop();
        }
        public bool restart()
        {
            stop();
            return internalConnect(type_, ip_, port_, userObject_, onPingTask_, pingIntval_);
        }
        private void threadSendRoutine()
        {
            while (Running)
            {
                Thread.Sleep(1);
                if ((socket_ == null) || (!IsConnect))
                {
                    continue;
                }
                socket_.update();
                NetEvent io;
                while (Input.TryDequeue(out io))
                {
                    int length = 0;
                    // serialize protobuf object to bytes and send to socket
                    var byteArray = finder_.serialize(io.msgID, io.pbo, out length);
                    if (byteArray != null)
                    {
                        try
                        {
                            var sentBytes = socket_.send(byteArray, length);
                            if (sentBytes <= 0)
                            {
                                Broken = true;
                                break;
                            }
                        }
                        catch (System.Exception)
                        {
                            Broken = true;
                            break;
                        }
                    }
                }
            }
        }
        private void threadRecvRoutine()
        {
            byte[] temp = new byte[4];
            byte[] recvBuffer = new byte[Config.RECV_BUF_SIZE];
            Ringbuffer ringBuffer = new Ringbuffer((uint)Config.RECV_BUF_SIZE);
            while (Running)
            {
                Thread.Sleep(1);
                if ((socket_ == null) || (!IsConnect))
                {
                    continue;
                }
                int bytes = 0;
                try
                {
                    bytes = socket_.receive(recvBuffer);
                    if (bytes < 0)
                    {
                        Broken = true;
                        return;
                    }
                }
                catch (System.Exception)
                {
                    Broken = true;
                    return;
                }
                if (bytes == 0)
                {
                    continue;
                }
                ringBuffer.write(recvBuffer, (uint)bytes);
                // split packet
                while (ringBuffer.available() >= 8)
                {
                    ringBuffer.copy(temp, 4);
                    var lengthNetworkOrder = BitConverter.ToInt32(temp, 0);
                    var length = IPAddress.NetworkToHostOrder(lengthNetworkOrder);
                    if (length > ringBuffer.available())
                    {
                        break;
                    }
                    ringBuffer.eat(4);
                    ringBuffer.copy(temp, 4);
                    var msgID = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(temp, 0));
                    ringBuffer.eat(4);
                    ringBuffer.read(recvBuffer, (uint)(length - 8));
                    NetEvent e;
                    e.msgID = (uint)msgID;
                    e.pbo = finder_.deserialize(recvBuffer, length - 8, (uint)msgID);
                    RecvPacket += 1;
                    Output.Enqueue(e);
                }
            }
        }
        #region PublicMethod
        private ISocket createSocket(int type)
        {
            switch (type)
            {
                case Network.TCPSocket:
                    return new TcpSocket();
                case Network.KCPSocket:
                    return new KcpSocket();
            }
            return null;
        }
        private bool internalConnect(int type, String ip, int port, object userObject = null, Network.OnPingTask pingTask = null, int pingIntval = 3000)
        {
            type_ = type;
            if (finder_ == null)
            {
                finder_ = new ProtocolFinder(this);
            }
            if (receiver_ == null)
            {
                receiver_ = new NetworkingReceiver();
            }
            ip_ = ip;
            port_ = port;
            socket_ = createSocket(type);
            userObject_ = userObject;
            running_ = true;
            broken_ = false;
            onPingTask_ = pingTask;
            pingIntval_ = pingIntval;
            sendThread_ = new Thread(threadSendRoutine);
            recvThread_ = new Thread(threadRecvRoutine);
            sendThread_.Start();
            recvThread_.Start();
            return connect();
        }
        private void internalStop()
        {
            running_ = false;
            if (recvThread_ != null)
            {
                recvThread_.Join();
            }
            if (sendThread_ != null)
            {
                sendThread_.Join();
            }
            recvThread_ = null;
            sendThread_ = null;
            if (socket_ != null)
            {
                socket_.close();
                socket_ = null;
            }
        }
        public int Type
        {
            get
            {
                return type_;
            }
        }
        public string Error
        {
            get
            {
                if (socket_ != null)
                {
                    return socket_.getError();
                }
                else
                {
                    return "";
                }
            }
        }
        public NetworkingReceiver Event
        {
            get
            {
                return receiver_;
            }
        }
        public bool IsConnect
        {
            get
            {
                if (socket_ == null)
                {
                    return false;
                }
                else
                {
                    return socket_.isConnect();
                }
            }
        }
        public int SentPacket
        {
            get
            {
                return SentPacket_;
            }
            set
            {
                SentPacket_ = value;
            }
        }
        public int RecvPacket
        {
            get
            {
                return RecvPacket_;
            }
            set
            {
                RecvPacket_ = value;
            }
        }
        private void checkPing()
        {
            if (onPingTask_ == null)
            {
                return;
            }
            pingDelay_ += Time.deltaTime;
            if ((int)(pingDelay_ * 1000.0f) >= pingIntval_)
            {
                pingDelay_ = .0f;
                onPingTask_(userObject_);
            }
        }
        public void loop()
        {
            if (!IsConnect)
            {
                return;
            }
            if (broken_)
            {
                stop();
                return;
            }
            checkPing();
            NetEvent e;
            while (output_.TryDequeue(out e))
            {
                receiver_.onMessage(e.msgID, (IMessage)e.pbo);
            }
        }
        public bool send(int msgID, object msg)
        {
            if (!IsConnect)
            {
                return false;
            }
            if (broken_)
            {
                stop();
                return false;
            }
            NetEvent e;
            e.msgID = (uint)msgID;
            e.pbo = msg;
            Input.Enqueue(e);
            return true;
        }
        public object UserObject
        {
            get
            {
                return userObject_;
            }
            set
            {
                userObject_ = value;
            }
        }
        #endregion

        #region PrivateMethod
        private ConcurrentQueue<NetEvent> Input
        {
            get
            {
                return input_;
            }
        }
        private ConcurrentQueue<NetEvent> Output
        {
            get
            {
                return output_;
            }
        }
        private ISocket ClientSocket
        {
            get
            {
                return socket_;
            }
        }
        private Boolean Broken
        {
            get
            {
                return broken_;
            }
            set
            {
                broken_ = value;
            }
        }
        private Boolean Running
        {
            get
            {
                return running_;
            }
        }
        private Network.OnPingTask PingTask
        {
            get
            {
                return onPingTask_;
            }
        }
        private int PingIntval
        {
            get
            {
                return pingIntval_;
            }
        }
        private bool connect()
        {
            return socket_.connect(ip_, port_);
        }
        #endregion
    }
}